package mh;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.text.ParseException;
import java.util.logging.Logger;

import mh.interfaces.Constants;
import mh.message.IncomingMessage;
import mh.message.MessageParser;

/**
 * This class manages the outgoing message of each move or action and also for the embedded chat.
 * 
 * @author Gianmarco Laggia
 * 
 */
public class ClientTCP implements Runnable, Constants {
	/**
	 * The Logger
	 */
	private static final Logger	LOG				= Logger.getLogger(ClientTCP.class.getName());
	/**
	 * The game in which is running this client.
	 */
	private final MagicHogwarts	game;

	/**
	 * The nickname of the player. used to add prefixes.
	 */
	private final String		nickname;
	/**
	 * The socket we use to connect to the TCP server.
	 */
	private Socket				socket;
	/**
	 * The input stream of data used to read messages from the server.
	 */
	private InputStreamReader	in;
	/**
	 * The output stream of data used to write messages to the server.
	 */
	private OutputStreamWriter	out;
	/**
	 * This variable is used to check if the socket is connected or not and cycle the thread.
	 */
	private boolean				connected		= false;

	private int					reconnections	= 1;

	private final String		server;
	private final int			port;
	private int					reconnDelay;

	/**
	 * Constructor
	 * 
	 * @param g
	 *            The game in which is running
	 * @param server
	 *            The address of the server
	 * @param port
	 *            The port to witch the server should be found
	 * @param nick
	 *            The nickname of the player
	 */
	public ClientTCP(MagicHogwarts g, String server, int port, String nick) {
		this(g, server, port, nick, 10000);
	}

	/**
	 * Constructor 2
	 * 
	 * @param g
	 *            The game in which is running
	 * @param server
	 *            The address of the server
	 * @param port
	 *            The port to witch the server should be found
	 * @param nick
	 *            The nickname of the player
	 * @param rDelay
	 *            The reconnection delay
	 */
	public ClientTCP(MagicHogwarts g, String server, int port, String nick, int rDelay) {
		if ((g == null) || (server == null) || server.equals("") || (nick == null) || (port < 1024) || "".equals(nick)) {
			throw new IllegalArgumentException();
		}
		this.server = server;
		this.port = port;
		this.game = g;
		this.socket = new Socket();
		this.nickname = nick;
		if (rDelay > 0) {
			this.reconnDelay = rDelay;
		} else {
			this.reconnDelay = 10000;
		}
	}

	@Override
	public void run() {
		while (this.connected) {

			String line = this.readFromSocket();
			if (line != null) {

				LOG.config("Received a string: " + line);

				IncomingMessage msg;
				try {
					msg = MessageParser.parseMessage(line);
					this.game.enqueueMessage(msg);
				} catch (ParseException e) {
					// ************ LOG ************ //
					LOG.severe("Error while parsing and incoming message see stack trace for details.");
					// ***************************** //
					e.printStackTrace();
				}

			} else {

				if (this.connected) {

					LOG.warning("Connection failed!");

					this.disconnect();

				}
			}
		}
	}

	/**
	 * Method used to bind a socket to the server.<br>
	 * Each time is called it creates a new socket.
	 */
	public void connect() {

		try {
			this.socket = new Socket();
			InetSocketAddress sa = new InetSocketAddress(this.server, this.port);
			this.socket.connect(sa, 500);
			this.connected = true;

			this.in = new InputStreamReader(this.socket.getInputStream());
			this.out = new OutputStreamWriter(this.socket.getOutputStream());
		} catch (ConnectException e) {
			LOG.severe("Error while connecting. " + e.getMessage());
			this.tryToReconnect();
		} catch (SocketTimeoutException e) {
			LOG.severe("Connection: " + e.getMessage() + ".");
			this.tryToReconnect();
		} catch (IOException e) {
			e.printStackTrace();
		}

	}

	private void tryToReconnect() {
		this.disconnect();

		LOG.info("I will try to reconnect in 10 seconds... (" + this.reconnections + "/10)");
		try {
			Thread.sleep(this.reconnDelay);
		} catch (InterruptedException e) {
		}

		if (this.reconnections < MAX_RECONNECTIONS) {
			this.reconnections++;
			this.connect();

		} else {
			LOG.warning("Reconnection failed, exeeded max reconnection tries. Shutting down.");
			this.disconnect();
			//			System.exit(0); TODO
			return;
		}

	}

	/**
	 * Method used to disconnect the socket from the server
	 */
	public void disconnect() {
		if (this.socket.isConnected()) {
			try {
				this.connected = false;
				this.socket.close();
				this.in.close();
				this.out.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			if (this.socket.isClosed()) {
				this.connected = false;
				LOG.config("The connection has been closed perfectly.");
			} else {
				this.disconnect();
			}
		}
	}

	/**
	 * This method receives in input a string and sends it through the output.
	 * 
	 * @param message
	 *            The message to send
	 */
	public void send(String message) {
		if (this.socket.isConnected()) {
			try {
				LOG.config("Outgoing message: " + message);
				this.out.write(":" + this.nickname + " " + message + "\r\n");
				this.out.flush();
			} catch (SocketException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * This method read an incoming IRC string message from bytes inputStream (is) of socket (sock). Bytes are read
	 * until a pattern \r\n has detected. This pattern signs the end of the message.
	 * 
	 * @return A string containing the incoming message
	 */
	private String readFromSocket() {

		char car = 0;
		String str = new String();

		try {

			int count = 0;

			count = this.in.read();
			car = (char) count;
			str = str.concat(new Character(car).toString());

			while (count != -1) {

				if (str.endsWith("\r\n")) {
					break;
				} else {

					count = this.in.read();
					car = (char) count;
					str = str.concat(new Character(car).toString());

				}
			}

		} catch (IOException e) {
			// ************ LOG ************ //
			LOG.warning("An error has occured reading something from the socket.");
			// ***************************** //
			e.printStackTrace(); // TODO check this...
		}

		if (str.length() > 1) {
			return str.trim();
		} else {
			return null;
		}

	}

	/**
	 * Checks if the socket is correctly connected.
	 * 
	 * @return true if positive
	 */
	public boolean isConnected() {
		return this.socket.isConnected();
	}

}
